Skip to content

fix(policy): resolve this.relation.field against @@allow model, unnest array columns in subqueries#2734

Open
hechang27-sprt wants to merge 2 commits into
zenstackhq:devfrom
hechang27-sprt:fix/this-authscope-relation-resolution
Open

fix(policy): resolve this.relation.field against @@allow model, unnest array columns in subqueries#2734
hechang27-sprt wants to merge 2 commits into
zenstackhq:devfrom
hechang27-sprt:fix/this-authscope-relation-resolution

Conversation

@hechang27-sprt

@hechang27-sprt hechang27-sprt commented Jun 26, 2026

Copy link
Copy Markdown

Fixes #2733.

Problem

Two bugs in the policy plugin expression transformer:

  1. this.relation.field resolves against the wrong model inside auth().collection?[...] predicates. The expression transformer uses context.thisType to resolve the field definition but passes restContext (with modelOrType still pointing to the collection model) to transformRelationAccess. This causes Field "X" not found in model "<collection>" crashes.

  2. x in this.relation.arrayField generates broken SQL on PostgreSQL. The expression compiles to = ANY((SELECT arrayCol FROM ...)) where the subquery returns array-typed rows (integer[]). PostgreSQL ANY(subquery) requires scalar rows, producing operator does not exist: integer = integer[].

Fix

Fix 1: Resolve this.relation.field against the @@allow model

Pass modelOrType: context.thisType and alias: context.thisAlias to transformRelationAccess when the receiver is this, so relation fields and table aliases resolve against the model bearing the policy. Also fixed getFieldDefFromFieldRef to resolve multi-segment this.relation.field chains (needed for the array-field detection in Fix 2).

Fix 2: Cross-database scalar IN relation.arrayField

Single entry point buildArrayInExists in _binary, with provider-specific handling:

Provider Approach Generated SQL
PostgreSQL unnest() in subquery column + = ANY(subquery) scalar = ANY(SELECT unnest("col") FROM "t" WHERE <join>)
SQLite Nested EXISTS with json_each EXISTS (SELECT 1 FROM "t" WHERE <join> AND EXISTS (SELECT 1 FROM json_each("t"."col") WHERE "value" = scalar))
MySQL Nested EXISTS with JSON_TABLE EXISTS (SELECT 1 FROM "t" WHERE <join> AND EXISTS (SELECT 1 FROM JSON_TABLE(...) AS jt WHERE jt."value" = scalar))

The SQLite/MySQL EXISTS rebuild preserves the original subquery FROM clause, so many-to-many joins are kept intact.

Verification

Two new PG-specific tests added to auth-access.test.ts:

Test Without fix With fix
this.author.level in auth().permissions?[p, ...] Field "author" not found in model "Permission" ✓ passes
this.id in this.group.visibleDocIds operator does not exist: integer = integer[] ✓ passes

All existing policy tests continue to pass on PostgreSQL, SQLite, and MySQL (no regressions).

…t array columns in subqueries

Fix zenstackhq#1: In _member() isThis branch, pass modelOrType: context.thisType
to transformRelationAccess() so that this.relation.field correctly
resolves relation fields against the @@Allow model (thisType) rather
than the collection predicate's model (modelOrType).

Fix zenstackhq#2: In _member() when the innermost field of a subquery selection
is an array type AND the provider is PostgreSQL, wrap the column with
unnest() so that ANY(subquery) receives scalar rows instead of array
rows. Other providers (SQLite, MySQL) leave the column as-is since
they store arrays differently (JSON) and would need different handling.
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The policy expression transformer now resolves chained this relation fields and compiles scalar in checks against array-column subqueries with provider-specific SQL. New e2e policy tests cover relation-field predicates and array-field access.

Changes

Policy relation and array IN fix

Layer / File(s) Summary
Chained this relation resolution
packages/plugins/policy/src/expression-transformer.ts
this member access now preserves context.thisType for nested relation traversal, array member traversal can emit unnest(...), and getFieldDefFromFieldRef resolves this.relation.field chains.
Array in subquery compilation
packages/plugins/policy/src/expression-transformer.ts
in expressions with array-column subqueries now call buildArrayInExists, which generates provider-specific EXISTS SQL for PostgreSQL, SQLite, and MySQL while preserving the subquery structure.
Policy e2e coverage
tests/e2e/orm/policy/auth-access.test.ts
New e2e cases assert this.author.level resolution inside auth().permissions? predicates and in checks against permission and group array fields in @@allow rules.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through this with a cheerful grin,
and found the array EXISTS tucked within.
PostgreSQL, SQLite, and MySQL sing,
while policy carrots do their springy thing.
Hooray for tests that make the burrow bright!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main policy-resolution and subquery-array fixes in this PR.
Linked Issues check ✅ Passed The changes resolve this member chains against the @@allow model and add tests covering relation and array membership cases.
Out of Scope Changes check ✅ Passed The added SQL and test updates are tied to the policy-resolution and subquery-membership fixes described in the issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/zenstackhq/zenstack/issues/comments/4808645116","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- review_stack_entry_start -->\n\n[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/zenstackhq/zenstack/pull/2734?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)\n\n<!-- review_stack_entry_end -->\n<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->\n\n> [!NOTE]\n> Currently processing new changes in this PR. This may take a few minutes, please wait...\n> \n> <details>\n> <summary>⚙️ Run configuration</summary>\n> \n> **Configuration used**: Path: .coderabbit.yaml\n> \n> **Review profile**: CHILL\n> \n> **Plan**: Pro\n> \n> **Run ID**: `6db5667e-2c09-4e16-9f54-cff855f6d775`\n> \n> </details>\n> \n> <details>\n> <summary>📥 Commits</summary>\n> \n> Reviewing files that changed from the base of the PR and between 8a317258b5e47ceabcf4a424b4453d77628f35e3 and 19658414e4c37a31a8affe6f015a7c668af5057a.\n> \n> </details>\n> \n> <details>\n> <summary>📒 Files selected for processing (2)</summary>\n> \n> * `packages/plugins/policy/src/expression-transformer.ts`\n> * `tests/e2e/orm/policy/auth-access.test.ts`\n> \n> </details>\n> \n> ```ascii\n>  ______________________________________________________________________________________________________________________________________________________________________________________________________\n> < Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it. - Brian Kernighan >\n>  ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\n>   \\\n>    \\   (\\__/)\n>        (•ㅅ•)\n>        /   づ\n> ```\n\n<!-- end of auto-generated comment: review in progress by coderabbit.ai -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=zenstackhq/zenstack&utm_content=2734)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>\n\n<!-- tips_end -->"},"request":{"retryCount":3,"signal":{},"retries":3,"retryAfter":16}}}

@hechang27-sprt hechang27-sprt force-pushed the fix/this-authscope-relation-resolution branch from 1965841 to cfbc2ce Compare June 27, 2026 01:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/plugins/policy/src/expression-transformer.ts`:
- Around line 1073-1082: The buildArrayInExists path is reconstructing a new
select from only the first table and blindly reading subquery.where, which
breaks many-to-many relation queries that carry correlation in joins and may
have no where clause. Update buildArrayInExists to work from the existing
SelectQueryNode shape, preserving any joins and other query parts instead of
using froms[0] alone, and append the containment predicate to the incoming
subquery rather than dereferencing subquery.where! or rebuilding the query from
scratch.
- Around line 712-715: The first `this` relation hop in
`expression-transformer.ts` is still carrying `restContext.alias`, which can
resolve to the wrong table alias in nested policy predicates. Update the
`transformRelationAccess` call in this branch so that when `modelOrType` is
switched back to `context.thisType`, the alias also uses `context.thisAlias` for
consistency with the one-segment `this.foo` handling, keeping the outer
`@@allow` model reference stable.
- Around line 257-262: The array-subquery branch in expression-transformer is
returning early before the enum scalar cast logic runs, so enum comparisons skip
the same normalization used by the non-subquery any(...) path. Update the
handling in ExpressionTransformer around buildArrayInExists so the right-hand
expression is still passed through the existing enum scalar casting logic before
routing to buildArrayInExists, keeping the cast behavior consistent for
array-column subqueries and regular any(...) comparisons.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4f6aeb4a-8549-4cdc-82d9-5aa6b985dfd8

📥 Commits

Reviewing files that changed from the base of the PR and between 805a6b8 and cfbc2ce.

📒 Files selected for processing (2)
  • packages/plugins/policy/src/expression-transformer.ts
  • tests/e2e/orm/policy/auth-access.test.ts

Comment thread packages/plugins/policy/src/expression-transformer.ts
Comment thread packages/plugins/policy/src/expression-transformer.ts
Comment thread packages/plugins/policy/src/expression-transformer.ts Outdated
@hechang27-sprt hechang27-sprt force-pushed the fix/this-authscope-relation-resolution branch 2 times, most recently from da34156 to 34d86b9 Compare June 27, 2026 02:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/plugins/policy/src/expression-transformer.ts (1)

1083-1128: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve joins when rebuilding the non-PostgreSQL EXISTS.

Line 1127 creates a fresh selectFrom(tableName), so any subquery.joins from relation access—especially many-to-many joins from transformManyToManyRelationAccess—are dropped. That can make SQLite/MySQL array membership checks uncorrelated or incorrect even though subquery.where is merged.

Verification script
#!/bin/bash
# Inspect relation subquery shapes and generated array-in helper call sites.
ast-grep outline packages/plugins/policy/src/expression-transformer.ts --match ExpressionTransformer --view expanded

rg -n -C4 'buildArrayInExists|transformManyToManyRelationAccess|subquery\.joins|selectFrom\(tableName\)' packages/plugins/policy/src/expression-transformer.ts
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/plugins/policy/src/expression-transformer.ts` around lines 1083 -
1128, The non-PostgreSQL EXISTS rebuild in expression-transformer drops relation
joins by creating a new selectFrom(tableName), which breaks many-to-many and
other correlated array checks. Update the logic around buildArrayInExists so it
reuses the existing subquery shape, including subquery.joins, when constructing
the final EXISTS for SQLite/MySQL, while still merging subquery.where with the
array membership predicate. Keep the original table/alias handling from
transformManyToManyRelationAccess and preserve all join conditions instead of
starting a fresh root query.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/plugins/policy/src/expression-transformer.ts`:
- Around line 1083-1128: The non-PostgreSQL EXISTS rebuild in
expression-transformer drops relation joins by creating a new
selectFrom(tableName), which breaks many-to-many and other correlated array
checks. Update the logic around buildArrayInExists so it reuses the existing
subquery shape, including subquery.joins, when constructing the final EXISTS for
SQLite/MySQL, while still merging subquery.where with the array membership
predicate. Keep the original table/alias handling from
transformManyToManyRelationAccess and preserve all join conditions instead of
starting a fresh root query.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e26234ba-921d-477d-8c75-4d3c1efeac5c

📥 Commits

Reviewing files that changed from the base of the PR and between da34156 and 34d86b9.

📒 Files selected for processing (2)
  • packages/plugins/policy/src/expression-transformer.ts
  • tests/e2e/orm/policy/auth-access.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/orm/policy/auth-access.test.ts

…-db array-in EXISTS

Fixes zenstackhq#2733.

Fix 1: `_member` isThis branch now passes `modelOrType: context.thisType`
to `transformRelationAccess`, so `this.relation.field` resolves against
the @@Allow model instead of the collection predicate model.

Fix 2: `this.scalarField in this.relation.arrayField` now generates a
cross-database EXISTS subquery instead of `= ANY(subquery)`. Each
provider uses its native array operator:
- PostgreSQL: EXISTS (SELECT 1 FROM t WHERE join AND scalar = ANY(t.col))
- SQLite:    EXISTS (... AND EXISTS (SELECT 1 FROM json_each(t.col) WHERE value = scalar))
- MySQL:     EXISTS (... AND EXISTS (SELECT 1 FROM JSON_TABLE(t.col, ...) AS jt WHERE jt.value = scalar))

Also fixes `getFieldDefFromFieldRef` to resolve multi-segment
`this.relation.field` chains, needed to detect array fields in the
`in`-operator RHS.

Tests: two new PG tests in auth-access.test.ts, verified to fail
without the fix and pass with it.
@hechang27-sprt hechang27-sprt force-pushed the fix/this-authscope-relation-resolution branch from 34d86b9 to 9e78de7 Compare June 27, 2026 02:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: this.relationField resolves against collection model inside auth().collection?[...] predicates

1 participant